/**
 * fshows.com
 * Copyright (C) 2013-2019 All Rights Reserved.
 */
package com.fshows.leshuapay.sdk.util;

import com.alibaba.fastjson.JSONObject;
import com.fshows.leshuapay.sdk.exception.LeshuaException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author zhaoyi
 * @version LeshuaSignature.java, v 0.1 2020-07-28 10:05 zhaoyi
 */
@Slf4j
public class LeshuaSignature {

    private static final String SECRET_KEY = "key";

    /**
     * 计算签名
     *
     * @param paramMap
     * @param exludedSignParams 计算签名排除的参数
     * @param key
     * @return
     */
    public static String getMD5Sign(Map<String, String> paramMap, Set<String> exludedSignParams, String key) {
        if (paramMap == null || paramMap.isEmpty()) {
            log.warn("待签名参数map为空!");
            return null;
        }
        String str2Sign = buildParam4Sign(paramMap, exludedSignParams, key);
        String result = DigestUtils.md5Hex(str2Sign).toUpperCase();
        log.debug("MD5签名原始串：{}，签名结果：{}", new Object[]{str2Sign, result});
        return result;
    }

    /**
     * 计算签名
     *
     * @param paramMap
     * @param exludedSignParams 计算签名排除的参数
     * @param key
     * @return
     */
    public static String generateMd5Sign(Map<String, Object> paramMap, Set<String> exludedSignParams, String key) {
        if (paramMap == null || paramMap.isEmpty()) {
            log.warn("待签名参数map为空!");
            return null;
        }
        String str2Sign = generateMd5SignStr(paramMap, exludedSignParams, key);
        String result = DigestUtils.md5Hex(str2Sign).toUpperCase();
        log.info("MD5签名原始串：{}，签名结果：{}", new Object[]{str2Sign, result});
        return result;
    }

    /**
     * 从API返回的XML数据里面重新计算一次签名
     *
     * @param responseString
     * @param exludedSignParams
     * @param key
     * @return
     * @throws DocumentException
     */
    public static String getSignFromResponseString(String responseString, Set<String> exludedSignParams, String key) throws DocumentException {
        Map<String, String> map = getMapFromXML(responseString);
        //将API返回的数据根据用签名算法进行计算新的签名，用来跟API返回的签名进行比较
        return LeshuaSignature.getMD5Sign(map, exludedSignParams, key);
    }

    /**
     * 检查api返回数据的签名
     *
     * @param responseString
     * @param exludedSignParams
     * @param key
     * @return
     * @throws DocumentException
     */
    public static boolean checkIsSignValidFromResponseString(String responseString, Set<String> exludedSignParams, String key)
            throws DocumentException {
        Map<String, String> map = getMapFromXML(responseString);
        String signFromAPIResponse = map.get("sign");
        if (StringUtils.isEmpty(signFromAPIResponse)) {
            log.error("API返回的数据签名数据不存在");
            return false;
        }
        log.debug("服务器回包里面的签名是：{}", signFromAPIResponse);
        // 将API返回的数据根据用签名算法进行计算新的签名，用来跟API返回的签名进行比较
        String signForAPIResponse = LeshuaSignature.getMD5Sign(map, exludedSignParams, key);

        if (!signForAPIResponse.equalsIgnoreCase(signFromAPIResponse)) {
            // 签名验不过，表示这个API返回的数据有可能已经被篡改了
            log.error("API返回的数据签名验证不通过，signForAPIResponse={}，signFromAPIResponse={}",
                    signForAPIResponse, signFromAPIResponse);
            return false;
        }
        return true;
    }

    /**
     * 拼接用于签名的参数
     *
     * @param paramMap
     * @param exludedSignParams
     * @param md5Key
     * @return
     */
    private static String buildParam4Sign(Map<String, String> paramMap, Set<String> exludedSignParams, String md5Key) {
        Set<String> keySet = paramMap.keySet();
        StringBuilder param = new StringBuilder(20 * keySet.size());
        String[] keys = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keys);
        for (String key : keys) {
            if (exludedSignParams != null && exludedSignParams.contains(key)) {
                continue;
            }
            Object value = paramMap.get(key);
            // 排除值为null的情况
            if (value != null) {
                param.append(key).append("=").append(value).append("&");
            }
        }
        param.append(SECRET_KEY).append("=").append(md5Key);
        return param.toString();
    }

    /**
     * 拼接用于签名的参数
     *
     * @param paramMap
     * @param exludedSignParams
     * @param md5Key
     * @return
     */
    private static String generateMd5SignStr(Map<String, Object> paramMap, Set<String> exludedSignParams, String md5Key) {
        Set<String> keySet = paramMap.keySet();
        StringBuilder param = new StringBuilder(20 * keySet.size());
        String[] keys = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keys);
        for (String key : keys) {
            if (exludedSignParams != null && exludedSignParams.contains(key)) {
                continue;
            }
            Object value = paramMap.get(key);
            // 排除值为null的情况
            if (value != null) {
                param.append(key).append("=").append(value).append("&");
            }
        }
        param.append(SECRET_KEY).append("=").append(md5Key);
        return param.toString();
    }

    /**
     * 将XML字符串转换成map
     *
     * @param xmlString
     * @return
     * @throws DocumentException
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static Map<String, String> getMapFromXML(String xmlString) throws DocumentException {
        // 这里用dom的方式解析回包的最主要目的是防止API新增回包字段
        org.dom4j.Document document = DocumentHelper.parseText(xmlString);
        org.dom4j.Element root = document.getRootElement();
        List<?> eleList = root.elements();
        if (eleList != null) {
            Map<String, String> map = new HashMap<String, String>(eleList.size());
            for (Object obj : eleList) {
                if (!(obj instanceof Element)) {
                    log.warn("节点[{}]不是Element，忽略！", obj);
                    continue;
                }
                Element ele = (Element) obj;
                map.put(ele.getName(), ele.getText());
            }
            return map;
        }
        return Collections.emptyMap();

    }


    /**
     * 计算base64签名，用于商户进件等
     *
     * @param agentKey
     * @param data
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getBase64Sign(String agentKey, JSONObject data) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        sb.append(SignUtil.SING_PREFIX).append(agentKey).append(data);
        return Base64.encodeBase64String(DigestUtils.md5Hex(sb.toString()).getBytes(SignUtil.DEFAULT_CHARSET));
    }

    public static String getRiskBase64Sign(String agentKey, String data) throws LeshuaException {
        StringBuilder sb = new StringBuilder();
        sb.append(SignUtil.SING_PREFIX).append(agentKey).append(data);
        try {
            return Base64.encodeBase64String(DigestUtils.md5Hex(sb.toString()).toLowerCase().getBytes(SignUtil.DEFAULT_CHARSET));
        } catch (Exception e) {
            throw new LeshuaException("签名获取失败", e);
        }
    }

    public static Set<String> reqExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<String>(1);
        excludedSignParams.add("sign");
        return excludedSignParams;
    }

    /**
     * 响应结果不参与计算签名的参数
     *
     * @return
     */
    public static Set<String> resExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<>(2);
        excludedSignParams.add("sign");
        excludedSignParams.add("resp_code");
        return excludedSignParams;
    }


    /**
     * 响应结果不参与计算签名的参数
     *
     * @return
     */
    public static Set<String> notifyExcludedSignParams() {
        Set<String> excludedSignParams = new HashSet<>(2);
        excludedSignParams.add("sign");
        excludedSignParams.add("error_code");
        return excludedSignParams;
    }
}
